Implement identity proof api (#1597)

* implement identity proof api

* fix warnings in AccountActivity

* fix createClickableText method

* improve error handling

* use combineOptionalLiveData to simplify code
This commit is contained in:
Konrad Pozniak 2019-12-21 18:56:16 +01:00 committed by GitHub
parent c253f6b23b
commit c44dd455b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 21 deletions

View File

@ -19,8 +19,7 @@ import android.animation.ArgbEvaluator
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.*
import android.graphics.PorterDuff
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -52,6 +51,8 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
@ -118,7 +119,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java] viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)) viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false) animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
@ -350,6 +351,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
}) })
viewModel.accountFieldData.observe(this, Observer<List<Either<IdentityProof, Field>>> {
accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged()
})
} }
/** /**
@ -378,7 +384,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView) val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
accountFieldAdapter.fields = account.fields ?: emptyList() // accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
@ -472,7 +478,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
// this is necessary because API 19 can't handle vector compound drawables // this is necessary because API 19 can't handle vector compound drawables
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate() val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
movedIcon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN) movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
} }

View File

@ -15,6 +15,7 @@
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import android.text.method.LinkMovementMethod
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@ -23,15 +24,17 @@ import android.widget.TextView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.CustomEmojiHelper import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.LinkHelper
import kotlinx.android.synthetic.main.item_account_field.view.* import kotlinx.android.synthetic.main.item_account_field.view.*
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() { class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
var emojis: List<Emoji> = emptyList() var emojis: List<Emoji> = emptyList()
var fields: List<Field> = emptyList() var fields: List<Either<IdentityProof, Field>> = emptyList()
override fun getItemCount() = fields.size override fun getItemCount() = fields.size
@ -41,18 +44,30 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView
} }
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val field = fields[position] val proofOrField = fields[position]
val emojifiedName = CustomEmojiHelper.emojifyString(field.name, emojis, viewHolder.nameTextView) if(proofOrField.isLeft()) {
viewHolder.nameTextView.text = emojifiedName val identityProof = proofOrField.asLeft()
val emojifiedValue = CustomEmojiHelper.emojifyText(field.value, emojis, viewHolder.valueTextView) viewHolder.nameTextView.text = identityProof.provider
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) viewHolder.valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl)
viewHolder.valueTextView.movementMethod = LinkMovementMethod.getInstance()
if(field.verifiedAt != null) {
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else { } else {
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 ) val field = proofOrField.asRight()
val emojifiedName = CustomEmojiHelper.emojifyString(field.name, emojis, viewHolder.nameTextView)
viewHolder.nameTextView.text = emojifiedName
val emojifiedValue = CustomEmojiHelper.emojifyText(field.value, emojis, viewHolder.valueTextView)
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener)
if(field.verifiedAt != null) {
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else {
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 )
}
} }
} }

View File

@ -115,10 +115,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
timestampInfo.append(""); timestampInfo.append("");
if (app.getWebsite() != null) { if (app.getWebsite() != null) {
URLSpan span = new CustomURLSpan(app.getWebsite()); CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite());
SpannableStringBuilder text = new SpannableStringBuilder(app.getName());
text.setSpan(span, 0, app.getName().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
timestampInfo.append(text); timestampInfo.append(text);
timestampInfo.setMovementMethod(LinkMovementMethod.getInstance()); timestampInfo.setMovementMethod(LinkMovementMethod.getInstance());
} else { } else {

View File

@ -0,0 +1,9 @@
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
data class IdentityProof(
val provider: String,
@SerializedName("provider_username") val username: String,
@SerializedName("profile_url") val profileUrl: String
)

View File

@ -318,6 +318,11 @@ interface MastodonApi {
@Query("id[]") accountIds: List<String> @Query("id[]") accountIds: List<String>
): Call<List<Relationship>> ): Call<List<Relationship>>
@GET("api/v1/accounts/{id}/identity_proofs")
fun identityProofs(
@Path("id") accountId: String
): Call<List<IdentityProof>>
@GET("api/v1/blocks") @GET("api/v1/blocks")
fun blocks( fun blocks(
@Query("max_id") maxId: String? @Query("max_id") maxId: String?

View File

@ -179,6 +179,14 @@ public class LinkHelper {
view.setMovementMethod(LinkMovementMethod.getInstance()); view.setMovementMethod(LinkMovementMethod.getInstance());
} }
public static CharSequence createClickableText(String text, String link) {
URLSpan span = new CustomURLSpan(link);
SpannableStringBuilder clickableText = new SpannableStringBuilder(text);
clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return clickableText;
}
/** /**
* Opens a link, depending on the settings, either in the browser or in a custom tab * Opens a link, depending on the settings, either in the browser or in a custom tab
* *

View File

@ -6,12 +6,11 @@ import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.appstore.*
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -27,6 +26,14 @@ class AccountViewModel @Inject constructor(
val accountData = MutableLiveData<Resource<Account>>() val accountData = MutableLiveData<Resource<Account>>()
val relationshipData = MutableLiveData<Resource<Relationship>>() val relationshipData = MutableLiveData<Resource<Relationship>>()
private val identityProofData = MutableLiveData<List<IdentityProof>>()
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right<IdentityProof, Field>(it) })
}
private val callList: MutableList<Call<*>> = mutableListOf() private val callList: MutableList<Call<*>> = mutableListOf()
private val disposable: Disposable = eventHub.events private val disposable: Disposable = eventHub.events
.subscribe { event -> .subscribe { event ->
@ -60,6 +67,7 @@ class AccountViewModel @Inject constructor(
} }
override fun onFailure(call: Call<Account>, t: Throwable) { override fun onFailure(call: Call<Account>, t: Throwable) {
Log.w(TAG, "failed obtaining account", t)
accountData.postValue(Error()) accountData.postValue(Error())
isDataLoading = false isDataLoading = false
isRefreshing.postValue(false) isRefreshing.postValue(false)
@ -90,6 +98,7 @@ class AccountViewModel @Inject constructor(
} }
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) { override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
Log.w(TAG, "failed obtaining relationships", t)
relationshipData.postValue(Error()) relationshipData.postValue(Error())
} }
}) })
@ -98,6 +107,30 @@ class AccountViewModel @Inject constructor(
} }
} }
private fun obtainIdentityProof(reload: Boolean = false) {
if (identityProofData.value == null || reload) {
val call = mastodonApi.identityProofs(accountId)
call.enqueue(object : Callback<List<IdentityProof>> {
override fun onResponse(call: Call<List<IdentityProof>>,
response: Response<List<IdentityProof>>) {
val proofs = response.body()
if (response.isSuccessful && proofs != null ) {
identityProofData.postValue(proofs)
} else {
identityProofData.postValue(emptyList())
}
}
override fun onFailure(call: Call<List<IdentityProof>>, t: Throwable) {
Log.w(TAG, "failed obtaining identity proofs", t)
}
})
callList.add(call)
}
}
fun changeFollowState() { fun changeFollowState() {
val relationship = relationshipData.value?.data val relationship = relationshipData.value?.data
if (relationship?.following == true || relationship?.requested == true) { if (relationship?.following == true || relationship?.requested == true) {
@ -227,6 +260,7 @@ class AccountViewModel @Inject constructor(
return return
accountId.let { accountId.let {
obtainAccount(isReload) obtainAccount(isReload)
obtainIdentityProof()
if (!isSelf) if (!isSelf)
obtainRelationship(isReload) obtainRelationship(isReload)
} }