mirror of
https://github.com/etesync/android
synced 2025-07-12 09:38:12 +00:00
Invitations: implement invitations handling.
This commit is contained in:
parent
b11ece37d5
commit
bf1155d0b8
@ -237,6 +237,10 @@
|
|||||||
android:name=".ui.etebase.CollectionActivity"
|
android:name=".ui.etebase.CollectionActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
/>
|
/>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.etebase.InvitationsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ViewCollectionActivity"
|
android:name=".ui.ViewCollectionActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
@ -45,6 +45,7 @@ import com.etesync.syncadapter.resource.LocalAddressBook
|
|||||||
import com.etesync.syncadapter.resource.LocalCalendar
|
import com.etesync.syncadapter.resource.LocalCalendar
|
||||||
import com.etesync.syncadapter.syncadapter.requestSync
|
import com.etesync.syncadapter.syncadapter.requestSync
|
||||||
import com.etesync.syncadapter.ui.etebase.CollectionActivity
|
import com.etesync.syncadapter.ui.etebase.CollectionActivity
|
||||||
|
import com.etesync.syncadapter.ui.etebase.InvitationsActivity
|
||||||
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
|
import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment
|
||||||
import com.etesync.syncadapter.utils.HintManager
|
import com.etesync.syncadapter.utils.HintManager
|
||||||
import com.etesync.syncadapter.utils.ShowcaseBuilder
|
import com.etesync.syncadapter.utils.ShowcaseBuilder
|
||||||
@ -156,6 +157,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.activity_account, menu)
|
menuInflater.inflate(R.menu.activity_account, menu)
|
||||||
|
if (settings.isLegacy) {
|
||||||
|
val invitations = menu.findItem(R.id.invitations)
|
||||||
|
invitations.setVisible(false)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,6 +190,10 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
|||||||
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
|
.setPositiveButton(android.R.string.yes) { _, _ -> }.create()
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
R.id.invitations -> {
|
||||||
|
val intent = InvitationsActivity.newIntent(this, account)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package com.etesync.syncadapter.ui.etebase
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import com.etesync.syncadapter.R
|
||||||
|
import com.etesync.syncadapter.ui.BaseActivity
|
||||||
|
|
||||||
|
class InvitationsActivity : BaseActivity() {
|
||||||
|
private lateinit var account: Account
|
||||||
|
private val model: AccountViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
account = intent.extras!!.getParcelable(EXTRA_ACCOUNT)!!
|
||||||
|
|
||||||
|
setContentView(R.layout.etebase_collection_activity)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
model.loadAccount(this, account)
|
||||||
|
title = getString(R.string.invitations_title)
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace(R.id.fragment_container, InvitationsListFragment())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val EXTRA_ACCOUNT = "account"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, account: Account): Intent {
|
||||||
|
val intent = Intent(context, InvitationsActivity::class.java)
|
||||||
|
intent.putExtra(EXTRA_ACCOUNT, account)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package com.etesync.syncadapter.ui.etebase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.*
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.ListFragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.observe
|
||||||
|
import com.etebase.client.CollectionAccessLevel
|
||||||
|
import com.etebase.client.FetchOptions
|
||||||
|
import com.etebase.client.SignedInvitation
|
||||||
|
import com.etebase.client.Utils
|
||||||
|
import com.etesync.syncadapter.R
|
||||||
|
import org.jetbrains.anko.doAsync
|
||||||
|
import org.jetbrains.anko.uiThread
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationsListFragment : ListFragment(), AdapterView.OnItemClickListener {
|
||||||
|
private val model: AccountViewModel by activityViewModels()
|
||||||
|
private val invitationsModel: InvitationsViewModel by viewModels()
|
||||||
|
|
||||||
|
private var emptyTextView: TextView? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.invitations_list, container, false)
|
||||||
|
|
||||||
|
//This is instead of setEmptyText() function because of Google bug
|
||||||
|
//See: https://code.google.com/p/android/issues/detail?id=21742
|
||||||
|
emptyTextView = view.findViewById<TextView>(android.R.id.empty)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setListAdapterInvitations(invitations: List<SignedInvitation>) {
|
||||||
|
val context = context
|
||||||
|
if (context != null) {
|
||||||
|
val listAdapter = InvitationsListAdapter(context)
|
||||||
|
setListAdapter(listAdapter)
|
||||||
|
|
||||||
|
listAdapter.addAll(invitations)
|
||||||
|
|
||||||
|
emptyTextView!!.setText(R.string.invitations_list_empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
model.observe(this) {
|
||||||
|
invitationsModel.loadInvitations(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
invitationsModel.observe(this) {
|
||||||
|
setListAdapterInvitations(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
listView.onItemClickListener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
|
||||||
|
invitationsModel.cancelLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(parent: AdapterView<*>, view_: View, position: Int, id: Long) {
|
||||||
|
val invitation = listAdapter?.getItem(position) as SignedInvitation
|
||||||
|
val fingerprint = Utils.prettyFingerprint(invitation.fromPubkey)
|
||||||
|
val view = layoutInflater.inflate(R.layout.invitation_alert_dialog, null)
|
||||||
|
view.findViewById<TextView>(R.id.body).text = getString(R.string.invitations_accept_reject_dialog)
|
||||||
|
view.findViewById<TextView>(R.id.fingerprint).text = fingerprint
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.invitations_title)
|
||||||
|
.setIcon(R.drawable.ic_email_black)
|
||||||
|
.setView(view)
|
||||||
|
.setNegativeButton(R.string.invitations_reject) { dialogInterface, i ->
|
||||||
|
invitationsModel.reject(model.value!!, invitation)
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.invitations_accept) { dialogInterface, i ->
|
||||||
|
invitationsModel.accept(model.value!!, invitation)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inner class InvitationsListAdapter(context: Context) : ArrayAdapter<SignedInvitation>(context, R.layout.invitations_list_item) {
|
||||||
|
|
||||||
|
override fun getView(position: Int, _v: View?, parent: ViewGroup): View {
|
||||||
|
var v = _v
|
||||||
|
if (v == null)
|
||||||
|
v = LayoutInflater.from(context).inflate(R.layout.invitations_list_item, parent, false)
|
||||||
|
|
||||||
|
val invitation = getItem(position)!!
|
||||||
|
|
||||||
|
val tv = v!!.findViewById<View>(R.id.title) as TextView
|
||||||
|
// FIXME: Should have a sensible string here
|
||||||
|
tv.text = "Invitation ${position}"
|
||||||
|
|
||||||
|
// FIXME: Also mark admins
|
||||||
|
val readOnly = v.findViewById<View>(R.id.read_only)
|
||||||
|
readOnly.visibility = if (invitation.accessLevel == CollectionAccessLevel.ReadOnly) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvitationsViewModel : ViewModel() {
|
||||||
|
private val invitations = MutableLiveData<List<SignedInvitation>>()
|
||||||
|
private var asyncTask: Future<Unit>? = null
|
||||||
|
|
||||||
|
fun loadInvitations(accountCollectionHolder: AccountHolder) {
|
||||||
|
asyncTask = doAsync {
|
||||||
|
val ret = LinkedList<SignedInvitation>()
|
||||||
|
val invitationManager = accountCollectionHolder.etebase.invitationManager
|
||||||
|
var iterator: String? = null
|
||||||
|
var done = false
|
||||||
|
while (!done) {
|
||||||
|
val chunk = invitationManager.listIncoming(FetchOptions().iterator(iterator).limit(30))
|
||||||
|
iterator = chunk.stoken
|
||||||
|
done = chunk.isDone
|
||||||
|
|
||||||
|
ret.addAll(chunk.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiThread {
|
||||||
|
invitations.value = ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun accept(accountCollectionHolder: AccountHolder, invitation: SignedInvitation) {
|
||||||
|
doAsync {
|
||||||
|
val invitationManager = accountCollectionHolder.etebase.invitationManager
|
||||||
|
invitationManager.accept(invitation)
|
||||||
|
val ret = invitations.value!!.filter { it != invitation }
|
||||||
|
|
||||||
|
uiThread {
|
||||||
|
invitations.value = ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reject(accountCollectionHolder: AccountHolder, invitation: SignedInvitation) {
|
||||||
|
doAsync {
|
||||||
|
val invitationManager = accountCollectionHolder.etebase.invitationManager
|
||||||
|
invitationManager.reject(invitation)
|
||||||
|
val ret = invitations.value!!.filter { it != invitation }
|
||||||
|
|
||||||
|
uiThread {
|
||||||
|
invitations.value = ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelLoad() {
|
||||||
|
asyncTask?.cancel(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observe(owner: LifecycleOwner, observer: (List<SignedInvitation>) -> Unit) =
|
||||||
|
invitations.observe(owner, observer)
|
||||||
|
}
|
29
app/src/main/res/layout/invitation_alert_dialog.xml
Normal file
29
app/src/main/res/layout/invitation_alert_dialog.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="18dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/body"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/invitations_accept_reject_dialog"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fingerprint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
tools:text="aaaa 1111 bbbb cccc dddd eeee\n1111" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
20
app/src/main/res/layout/invitations_list.xml
Normal file
20
app/src/main/res/layout/invitations_list.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@id/android:list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@id/android:empty"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/invitations_loading"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
|
||||||
|
</LinearLayout>
|
27
app/src/main/res/layout/invitations_list_item.xml
Normal file
27
app/src/main/res/layout/invitations_list_item.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
tools:text="Title" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/read_only"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:src="@drawable/ic_readonly_dark"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -25,6 +25,10 @@
|
|||||||
android:title="@string/account_show_fingerprint"
|
android:title="@string/account_show_fingerprint"
|
||||||
app:showAsAction="ifRoom"/>
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
|
<item android:id="@+id/invitations"
|
||||||
|
android:title="@string/invitations_title"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
<item android:id="@+id/delete_account"
|
<item android:id="@+id/delete_account"
|
||||||
android:title="@string/account_delete"
|
android:title="@string/account_delete"
|
||||||
app:showAsAction="never"/>
|
app:showAsAction="never"/>
|
||||||
|
@ -168,6 +168,14 @@
|
|||||||
<string name="collection_members_remove">Would you like to revoke %s\'s access?\nPlease be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.</string>
|
<string name="collection_members_remove">Would you like to revoke %s\'s access?\nPlease be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information.</string>
|
||||||
<string name="collection_members_remove_admin">Removing access to admins is currently not supported.</string>
|
<string name="collection_members_remove_admin">Removing access to admins is currently not supported.</string>
|
||||||
|
|
||||||
|
<!-- Invitations -->
|
||||||
|
<string name="invitations_title">Invitations</string>
|
||||||
|
<string name="invitations_loading">Loading invitations...</string>
|
||||||
|
<string name="invitations_list_empty">No invitations</string>
|
||||||
|
<string name="invitations_accept_reject_dialog">Would you like to accept or reject the invitation?</string>
|
||||||
|
<string name="invitations_accept">Accept</string>
|
||||||
|
<string name="invitations_reject">Reject</string>
|
||||||
|
|
||||||
<!-- JournalItemActivity -->
|
<!-- JournalItemActivity -->
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
<string name="journal_item_tab_main">Main</string>
|
<string name="journal_item_tab_main">Main</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user