diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 4610420f..76b228e7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -25,6 +25,7 @@ import android.graphics.Color import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.os.Bundle +import android.text.Editable import android.view.Menu import android.view.MenuItem import android.view.View @@ -142,6 +143,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (viewModel.isSelf) { updateButtons() + saveNoteInfo.hide() + } else { + saveNoteInfo.visibility = View.INVISIBLE } } @@ -348,8 +352,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI viewModel.accountFieldData.observe(this, Observer>> { accountFieldAdapter.fields = it accountFieldAdapter.notifyDataSetChanged() - }) + viewModel.noteSaved.observe(this) { + saveNoteInfo.visible(it, View.INVISIBLE) + } } /** @@ -636,9 +642,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI subscribing = relation.subscribing } + accountNoteTextInputLayout.visible(relation.note != null) + accountNoteTextInputLayout.editText?.setText(relation.note) + + // add the listener late to avoid it firing on the first change + accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) + updateButtons() } + private val noteWatcher = object: DefaultTextWatcher() { + override fun afterTextChanged(s: Editable) { + viewModel.noteChanged(s.toString()) + } + } + private fun updateFollowButton() { if (viewModel.isSelf) { accountFollowButton.setText(R.string.action_edit_own_profile) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index 60561914..8bb2e88a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -100,7 +100,7 @@ class ReportViewModel @Inject constructor( val ids = listOf(accountId) muteStateMutable.value = Loading() blockStateMutable.value = Loading() - mastodonApi.relationshipsObservable(ids) + mastodonApi.relationships(ids) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( @@ -129,9 +129,9 @@ class ReportViewModel @Inject constructor( fun toggleMute() { val alreadyMuted = muteStateMutable.value?.data == true if (alreadyMuted) { - mastodonApi.unmuteAccountObservable(accountId) + mastodonApi.unmuteAccount(accountId) } else { - mastodonApi.muteAccountObservable(accountId) + mastodonApi.muteAccount(accountId) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -154,9 +154,9 @@ class ReportViewModel @Inject constructor( fun toggleBlock() { val alreadyBlocked = blockStateMutable.value?.data == true if (alreadyBlocked) { - mastodonApi.unblockAccountObservable(accountId) + mastodonApi.unblockAccount(accountId) } else { - mastodonApi.blockAccountObservable(accountId) + mastodonApi.blockAccount(accountId) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt index 96b672ea..cdbc3e81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt @@ -27,4 +27,5 @@ data class Relationship ( @SerializedName("showing_reblogs") val showingReblogs: Boolean, val subscribing: Boolean? = null, // Pleroma extension @SerializedName("domain_blocking") val blockingDomain: Boolean + val note: String? // nullable for backward compatibility / feature detection ) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index 3d689c6b..a037c9ef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -49,7 +49,6 @@ import retrofit2.Response import java.io.IOException import javax.inject.Inject - class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { @Inject @@ -113,28 +112,18 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { } } - override fun onMute(mute: Boolean, id: String, position: Int) { - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onMuteSuccess(mute, id, position) - } else { - onMuteFailure(mute, id) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onMuteFailure(mute, id) - } - } - - val call = if (!mute) { + override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { + if (!mute) { api.unmuteAccount(id) } else { api.muteAccount(id) } - callList.add(call) - call.enqueue(callback) + .autoDispose(from(this)) + .subscribe({ + onMuteSuccess(mute, id, position, notifications) + }, { + onMuteFailure(mute, id, notifications) + }) } private fun onMuteSuccess(muted: Boolean, id: String, position: Int) { @@ -164,27 +153,17 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { } override fun onBlock(block: Boolean, id: String, position: Int) { - val cb = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onBlockSuccess(block, id, position) - } else { - onBlockFailure(block, id) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onBlockFailure(block, id) - } - } - - val call = if (!block) { + if (!block) { api.unblockAccount(id) } else { api.blockAccount(id) } - callList.add(call) - call.enqueue(cb) + .autoDispose(from(this)) + .subscribe({ + onBlockSuccess(block, id, position) + }, { + onBlockFailure(block, id) + }) } private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { @@ -366,6 +345,28 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { } } +<<<<<<< HEAD +======= + private fun fetchRelationships(ids: List) { + api.relationships(ids) + .autoDispose(from(this)) + .subscribe(::onFetchRelationshipsSuccess) { + onFetchRelationshipsFailure(ids) + } + } + + private fun onFetchRelationshipsSuccess(relationships: List) { + val mutesAdapter = adapter as MutesAdapter + val mutingNotificationsMap = HashMap() + relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) } + mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap) + } + + private fun onFetchRelationshipsFailure(ids: List) { + Log.e(TAG, "Fetch failure for relationships of accounts: $ids") + } + +>>>>>>> ce973ea7... Personal account notes (#1978) private fun onFetchAccountsFailure(throwable: Throwable) { fetching = false Log.e(TAG, "Fetch failure", throwable) diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index bcf7ad83..8378bee6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -271,7 +271,7 @@ interface MastodonApi { @GET("api/v1/accounts/{id}") fun account( @Path("id") accountId: String - ): Call + ): Single /** * Method to fetch statuses for the specified account. @@ -310,42 +310,43 @@ interface MastodonApi { fun followAccount( @Path("id") accountId: String, @Field("reblogs") showReblogs: Boolean - ): Call + ): Single @POST("api/v1/accounts/{id}/unfollow") fun unfollowAccount( @Path("id") accountId: String - ): Call + ): Single @POST("api/v1/accounts/{id}/block") fun blockAccount( @Path("id") accountId: String - ): Call + ): Single @POST("api/v1/accounts/{id}/unblock") fun unblockAccount( @Path("id") accountId: String - ): Call + ): Single @POST("api/v1/accounts/{id}/mute") fun muteAccount( - @Path("id") accountId: String - ): Call + @Path("id") accountId: String, + @Field("notifications") notifications: Boolean? = null + ): Single @POST("api/v1/accounts/{id}/unmute") fun unmuteAccount( @Path("id") accountId: String - ): Call + ): Single @GET("api/v1/accounts/relationships") fun relationships( @Query("id[]") accountIds: List - ): Call> + ): Single> @GET("api/v1/accounts/{id}/identity_proofs") fun identityProofs( @Path("id") accountId: String - ): Call> + ): Single> @POST("api/v1/pleroma/accounts/{id}/subscribe") fun subscribeAccount( @@ -519,6 +520,7 @@ interface MastodonApi { @Field("choices[]") choices: List ): Single +<<<<<<< HEAD @POST("api/v1/accounts/{id}/block") fun blockAccountObservable( @Path("id") accountId: String @@ -554,6 +556,8 @@ interface MastodonApi { @Query("id[]") accountIds: List ): Single> +======= +>>>>>>> ce973ea7... Personal account notes (#1978) @FormUrlEncoded @POST("api/v1/reports") fun reportObservable( @@ -673,8 +677,18 @@ interface MastodonApi { @Path("id") accountId: String ): Single +<<<<<<< HEAD @GET("api/v1/pleroma/chats/{id}") fun getChat( @Path("id") chatId: String ): Single +======= + @FormUrlEncoded + @POST("api/v1/accounts/{id}/note") + fun updateAccountNote( + @Path("id") accountId: String, + @Field("comment") note: String + ): Single + +>>>>>>> ce973ea7... Personal account notes (#1978) } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index c830f187..24f0066c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -15,17 +15,14 @@ package com.keylesspalace.tusky.network +import android.util.Log import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll -import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Status import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import java.lang.IllegalStateException import android.util.Log @@ -96,36 +93,37 @@ class TimelineCasesImpl( } } - override fun mute(id: String) { - val call = mastodonApi.muteAccount(id) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) {} - - override fun onFailure(call: Call, t: Throwable) {} - }) - eventHub.dispatch(MuteEvent(id, true)) - } - - override fun muteStatus(status: Status, mute: Boolean) { + override fun muteConversation(status: Status, mute: Boolean): Single { val id = status.actionableId - - (if (mute) { - mastodonApi.muteStatus(id) + + val call = if (mute) { + mastodonApi.muteConversation(id) } else { - mastodonApi.unmuteStatus(id) - }).subscribe( { status -> - eventHub.dispatch(MuteStatusEvent(status.id, mute)) - }, {}).addTo(this.cancelDisposable) + mastodonApi.unmuteConversation(id) + } + return call.doAfterSuccess { + eventHub.dispatch(MuteConversationEvent(status.id, mute)) + } } - override fun block(id: String) { - val call = mastodonApi.blockAccount(id) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) {} + override fun mute(id: String, notifications: Boolean) { + mastodonApi.muteAccount(id, notifications) + .subscribe({ + eventHub.dispatch(MuteEvent(id)) + }, { t -> + Log.w("Failed to mute account", t) + }) + .addTo(cancelDisposable) + } - override fun onFailure(call: Call, t: Throwable) {} - }) - eventHub.dispatch(BlockEvent(id)) + override fun block(id: String) { + mastodonApi.blockAccount(id) + .subscribe({ + eventHub.dispatch(BlockEvent(id)) + }, { t -> + Log.w("Failed to block account", t) + }) + .addTo(cancelDisposable) } override fun delete(id: String): Single { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index 5f1d74b9..88b1f0ca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -2,7 +2,6 @@ package com.keylesspalace.tusky.viewmodel import android.util.Log import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Account @@ -11,21 +10,25 @@ import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.* +import io.reactivex.Single import io.reactivex.disposables.Disposable import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import java.util.concurrent.TimeUnit import javax.inject.Inject class AccountViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub, private val accountManager: AccountManager -) : ViewModel() { +) : RxAwareViewModel() { val accountData = MutableLiveData>() val relationshipData = MutableLiveData>() + val noteSaved = MutableLiveData() + private val identityProofData = MutableLiveData>() val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs -> @@ -33,48 +36,40 @@ class AccountViewModel @Inject constructor( .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) } - - private val callList: MutableList> = mutableListOf() - private val disposable: Disposable = eventHub.events - .subscribe { event -> - if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { - accountData.postValue(Success(event.newProfileData)) - } - } - val isRefreshing = MutableLiveData().apply { value = false } private var isDataLoading = false lateinit var accountId: String var isSelf = false + private var noteDisposable: Disposable? = null + + init { + eventHub.events + .subscribe { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) { + accountData.postValue(Success(event.newProfileData)) + } + }.autoDispose() + } + private fun obtainAccount(reload: Boolean = false) { if (accountData.value == null || reload) { isDataLoading = true accountData.postValue(Loading()) - val call = mastodonApi.account(accountId) - call.enqueue(object : Callback { - override fun onResponse(call: Call, - response: Response) { - if (response.isSuccessful) { - accountData.postValue(Success(response.body())) - } else { + mastodonApi.account(accountId) + .subscribe({ account -> + accountData.postValue(Success(account)) + isDataLoading = false + isRefreshing.postValue(false) + }, {t -> + Log.w(TAG, "failed obtaining account", t) accountData.postValue(Error()) - } - isDataLoading = false - isRefreshing.postValue(false) - } - - override fun onFailure(call: Call, t: Throwable) { - Log.w(TAG, "failed obtaining account", t) - accountData.postValue(Error()) - isDataLoading = false - isRefreshing.postValue(false) - } - }) - - callList.add(call) + isDataLoading = false + isRefreshing.postValue(false) + }) + .autoDispose() } } @@ -83,51 +78,27 @@ class AccountViewModel @Inject constructor( relationshipData.postValue(Loading()) - val ids = listOf(accountId) - val call = mastodonApi.relationships(ids) - call.enqueue(object : Callback> { - override fun onResponse(call: Call>, - response: Response>) { - val relationships = response.body() - if (response.isSuccessful && relationships != null && relationships.getOrNull(0) != null) { - val relationship = relationships[0] - relationshipData.postValue(Success(relationship)) - } else { + mastodonApi.relationships(listOf(accountId)) + .subscribe({ relationships -> + relationshipData.postValue(Success(relationships[0])) + }, { t -> + Log.w(TAG, "failed obtaining relationships", t) relationshipData.postValue(Error()) - } - } - - override fun onFailure(call: Call>, t: Throwable) { - Log.w(TAG, "failed obtaining relationships", t) - relationshipData.postValue(Error()) - } - }) - - callList.add(call) + }) + .autoDispose() } } private fun obtainIdentityProof(reload: Boolean = false) { if (identityProofData.value == null || reload) { - val call = mastodonApi.identityProofs(accountId) - call.enqueue(object : Callback> { - override fun onResponse(call: Call>, - response: Response>) { - val proofs = response.body() - if (response.isSuccessful && proofs != null ) { + mastodonApi.identityProofs(accountId) + .subscribe({ proofs -> identityProofData.postValue(proofs) - } else { - identityProofData.postValue(emptyList()) - } - } - - override fun onFailure(call: Call>, t: Throwable) { - Log.w(TAG, "failed obtaining identity proofs", t) - } - }) - - callList.add(call) + }, { t -> + Log.w(TAG, "failed obtaining identity proofs", t) + }) + .autoDispose() } } @@ -237,11 +208,17 @@ class AccountViewModel @Inject constructor( relationshipData.postValue(Loading(newRelation)) } - val callback = object : Callback { - override fun onResponse(call: Call, - response: Response) { - val relationship = response.body() - if (response.isSuccessful && relationship != null) { + when (relationshipAction) { + RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true) + RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) + RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) + RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) + RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true) + RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) + RelationShipAction.SUBSCRIBE -> mastodonApi.subscribeAccount(accountId) + RelationShipAction.UNSUBSCRIBE -> mastodonApi.unsubscribeAccount(accountId) + }.subscribe( + { relationship -> relationshipData.postValue(Success(relationship)) when (relationshipAction) { @@ -252,38 +229,35 @@ class AccountViewModel @Inject constructor( else -> { } } - - } else { + }, + { relationshipData.postValue(Error(relation)) } + ) + .autoDispose() + } - } - - override fun onFailure(call: Call, t: Throwable) { - relationshipData.postValue(Error(relation)) - } - } - - val call = when (relationshipAction) { - RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs) - RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) - RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) - RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) - RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId) - RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) - RelationShipAction.SUBSCRIBE -> mastodonApi.subscribeAccount(accountId) - RelationShipAction.UNSUBSCRIBE -> mastodonApi.unsubscribeAccount(accountId) - } - - call.enqueue(callback) - callList.add(call) + fun noteChanged(newNote: String) { + noteSaved.postValue(false) + noteDisposable?.dispose() + noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS) + .flatMap { + mastodonApi.updateAccountNote(accountId, newNote) + } + .doOnSuccess { + noteSaved.postValue(true) + } + .delay(4, TimeUnit.SECONDS) + .subscribe({ + noteSaved.postValue(false) + }, { + Log.e(TAG, "Error updating note", it) + }) } override fun onCleared() { - callList.forEach { - it.cancel() - } - disposable.dispose() + super.onCleared() + noteDisposable?.dispose() } fun refresh() { diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 285f7f93..dd03ea8f 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -218,16 +218,43 @@ app:barrierDirection="bottom" app:constraint_referenced_ids="accountAdminTextView,accountModeratorTextView,accountFollowsYouTextView,accountBadgeTextView" /> + + + + + + + + + Show link previews in timelines Show confirmation dialog before boosting Hide the title of the top toolbar + Your private note about this account + Saved!