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 android.os.Bundle
import androidx.activity.viewModels 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.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.compose.ListSettingsComposables.PrincipalList import org.tasks.compose.ListSettingsComposables.PrincipalList
import org.tasks.compose.ShareInvite.ShareInviteDialog
import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV 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 private val canRemovePrincipals: Boolean
@ -81,7 +112,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
} }
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) { 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( override suspend fun updateNameAndColor(
@ -100,6 +131,17 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
viewModel.deleteCalendar(caldavAccount, caldavCalendar) 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 { companion object {
val CaldavAccount.canRemovePrincipal: Boolean val CaldavAccount.canRemovePrincipal: Boolean
@ -107,5 +149,11 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
SERVER_TASKS, SERVER_OWNCLOUD, SERVER_SABREDAV -> true SERVER_TASKS, SERVER_OWNCLOUD, SERVER_SABREDAV -> true
else -> false 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 kotlinx.coroutines.withContext
import org.tasks.data.CaldavAccount import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar 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.CaldavDao
import org.tasks.data.Principal import org.tasks.data.Principal
import org.tasks.data.PrincipalDao import org.tasks.data.PrincipalDao
import org.tasks.sync.SyncAdapters
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -25,12 +29,19 @@ class CaldavCalendarViewModel @Inject constructor(
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val principalDao: PrincipalDao, private val principalDao: PrincipalDao,
private val taskDeleter: TaskDeleter, private val taskDeleter: TaskDeleter,
private val syncAdapters: SyncAdapters,
) : ViewModel() { ) : ViewModel() {
val error = MutableLiveData<Throwable?>() val error = MutableLiveData<Throwable?>()
val inFlight = MutableLiveData(false) val inFlight = MutableLiveData(false)
val finish = MutableLiveData<Intent>() 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 { doRequest {
val url = withContext(Dispatchers.IO) { val url = withContext(Dispatchers.IO) {
provider.forAccount(caldavAccount).makeCollection(name, color) provider.forAccount(caldavAccount).makeCollection(name, color)
@ -44,7 +55,10 @@ class CaldavCalendarViewModel @Inject constructor(
setIcon(icon) setIcon(icon)
caldavDao.insert(this) 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( suspend fun updateCalendar(
@ -77,6 +91,23 @@ class CaldavCalendarViewModel @Inject constructor(
finish.value = Intent(TaskListFragment.ACTION_DELETED) 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) = suspend fun removeUser(account: CaldavAccount, list: CaldavCalendar, principal: Principal) =
doRequest { doRequest {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -94,6 +125,7 @@ class CaldavCalendarViewModel @Inject constructor(
try { try {
return@withContext action() return@withContext action()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e)
error.value = e error.value = e
return@withContext null return@withContext null
} finally { } finally {

@ -235,6 +235,41 @@ open class CaldavClient(
return this 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( suspend fun removePrincipal(
account: CaldavAccount, account: CaldavAccount,
calendar: CaldavCalendar, calendar: CaldavCalendar,

@ -1,9 +1,43 @@
package org.tasks.compose 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 androidx.compose.ui.unit.dp
import java.util.*
object Constants { object Constants {
const val ICON_ALPHA = 0.54f const val ICON_ALPHA = 0.54f
val KEYLINE_FIRST = 16.dp val KEYLINE_FIRST = 16.dp
val HALF_KEYLINE = 8.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 package org.tasks.data
import androidx.lifecycle.LiveData 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 @Dao
interface PrincipalDao { interface PrincipalDao {
@Insert
fun insert(principal: Principal)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(principal: List<Principal>) fun insert(principals: List<Principal>)
@Query(""" @Query("""
DELETE DELETE

@ -207,6 +207,7 @@ object CustomIcons {
1182 to R.drawable.ic_outline_people_outline_24, 1182 to R.drawable.ic_outline_people_outline_24,
1183 to R.drawable.ic_outline_forum_24, 1183 to R.drawable.ic_outline_forum_24,
1184 to R.drawable.ic_twitter_logo_black, 1184 to R.drawable.ic_twitter_logo_black,
1185 to R.drawable.ic_outline_person_add_24,
) )
@JvmStatic @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"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_layout" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:layout_height="match_parent" android:id="@+id/root_layout"
android:descendantFocusability="beforeDescendants" android:layout_width="match_parent"
android:focusableInTouchMode="true" android:layout_height="match_parent"
android:orientation="vertical"> android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:orientation="vertical">
<include layout="@layout/toolbar"/> <include layout="@layout/toolbar"/>
<include layout="@layout/progress_view"/> <include layout="@layout/progress_view"/>
<ScrollView <FrameLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout <ScrollView
android:id="@+id/name_layout" android:layout_width="fill_parent"
style="@style/TagSettingsRow" android:layout_height="wrap_content">
android:hint="@string/display_name">
<com.google.android.material.textfield.TextInputEditText <LinearLayout
android:id="@+id/name" android:layout_width="match_parent"
style="@style/TagSettingsEditText" android:layout_height="wrap_content"
android:inputType="textCapSentences|textAutoCorrect" /> 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 <include layout="@layout/list_settings_color"/>
android:id="@+id/people"
android:layout_width="match_parent" <include layout="@layout/list_settings_icon"/>
android:layout_height="wrap_content"/>
<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> </LinearLayout>

@ -691,6 +691,9 @@ File %1$s contained %2$s.\n\n
<string name="list_members">List members</string> <string name="list_members">List members</string>
<string name="remove_user">Remove user?</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="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_declined">Invite declined</string>
<string name="invite_awaiting_response">Invite awaiting response</string> <string name="invite_awaiting_response">Invite awaiting response</string>
<string name="invite_invalid">Invite invalid</string> <string name="invite_invalid">Invite invalid</string>

Loading…
Cancel
Save